package com.zlyx.easy.swagger.utils;

import static com.google.common.base.Optional.fromNullable;
import static com.google.common.collect.Lists.newArrayList;
import static org.springframework.core.annotation.AnnotationUtils.findAnnotation;
import static org.springframework.core.annotation.AnnotationUtils.getAnnotation;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;

import com.fasterxml.classmate.ResolvedType;
import com.fasterxml.classmate.TypeResolver;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.zlyx.easy.swagger.annotations.SpringMapping;
import com.zlyx.easy.swagger.config.EasySwaggerConfiguration;

import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;

public class Annotations {

	private Annotations() {
		throw new UnsupportedOperationException();
	}

	public static Optional<ApiParam> findApiParamAnnotation(AnnotatedElement annotated) {
		return fromNullable(getAnnotation(annotated, ApiParam.class));
	}

	public static List<ApiResponses> findApiResponsesAnnotations(AnnotatedElement annotated) {
		List<ApiResponses> results = newArrayList();
		ApiResponses currentLevel = getAnnotation(annotated, ApiResponses.class);
		if (currentLevel != null) {
			results.add(currentLevel);
		}
		if (annotated instanceof Method) {
			ApiResponses parentLevel = findAnnotation(((Method) annotated).getDeclaringClass(), ApiResponses.class);
			if (parentLevel != null) {
				results.add(parentLevel);
			}
		}
		return results;
	}

	public static Function<SpringMapping, ResolvedType> resolvedTypeFromOperation(final TypeResolver typeResolver,
			final ResolvedType defaultType) {

		return new Function<SpringMapping, ResolvedType>() {
			@Override
			public ResolvedType apply(SpringMapping annotation) {
				return getResolvedType(annotation, typeResolver, defaultType);
			}
		};
	}

	public static Function<ApiResponse, ResolvedType> resolvedTypeFromResponse(final TypeResolver typeResolver,
			final ResolvedType defaultType) {

		return new Function<ApiResponse, ResolvedType>() {
			@Override
			public ResolvedType apply(ApiResponse annotation) {
				return getResolvedType(annotation, typeResolver, defaultType);
			}
		};
	}

	@VisibleForTesting
	static ResolvedType getResolvedType(SpringMapping annotation, TypeResolver resolver, ResolvedType defaultType) {

		if (null != annotation) {
			Class<?> response = annotation.response() != Void.class ? annotation.response()
					: EasySwaggerConfiguration.getDefaultModel();
			String responseContainer = annotation.responseContainer();
			if (resolvedType(resolver, response, responseContainer).isPresent()) {
				return resolvedType(resolver, response, responseContainer).get();
			}
		}
		return defaultType;
	}

	/**
	 * Finds first annotation of the given type on the given bean property and
	 * returns it. Search precedence is getter, setter, field.
	 *
	 * @param beanPropertyDefinition introspected jackson property definition
	 * @param annotationClass        class object representing desired annotation
	 * @param                        <A> type that extends Annotation
	 * @return first annotation found for property
	 */
	public static <A extends Annotation> Optional<A> findPropertyAnnotation(
			BeanPropertyDefinition beanPropertyDefinition, Class<A> annotationClass) {

		return tryGetFieldAnnotation(beanPropertyDefinition, annotationClass)
				.or(tryGetGetterAnnotation(beanPropertyDefinition, annotationClass))
				.or(tryGetSetterAnnotation(beanPropertyDefinition, annotationClass));
	}

	public static boolean memberIsUnwrapped(AnnotatedMember member) {
		if (member == null) {
			return false;
		}
		return Optional.fromNullable(member.getAnnotation(JsonUnwrapped.class)).isPresent();
	}

	public static String unwrappedPrefix(AnnotatedMember member) {
		if (member == null) {
			return "";
		}

		return Optional.fromNullable(member.getAnnotation(JsonUnwrapped.class))
				.transform(new Function<JsonUnwrapped, String>() {
					@Override
					public String apply(JsonUnwrapped input) {
						return input.prefix();
					}
				}).or("");
	}

	private static <A extends Annotation> Optional<A> tryGetGetterAnnotation(
			BeanPropertyDefinition beanPropertyDefinition, Class<A> annotationClass) {

		if (beanPropertyDefinition.hasGetter()) {
			return Optional.fromNullable(beanPropertyDefinition.getGetter().getAnnotation(annotationClass));
		}
		return Optional.absent();
	}

	private static <A extends Annotation> Optional<A> tryGetSetterAnnotation(
			BeanPropertyDefinition beanPropertyDefinition, Class<A> annotationClass) {

		if (beanPropertyDefinition.hasSetter()) {
			return Optional.fromNullable(beanPropertyDefinition.getSetter().getAnnotation(annotationClass));
		}
		return Optional.absent();
	}

	private static <A extends Annotation> Optional<A> tryGetFieldAnnotation(
			BeanPropertyDefinition beanPropertyDefinition, Class<A> annotationClass) {

		if (beanPropertyDefinition.hasField()) {
			return Optional.fromNullable(beanPropertyDefinition.getField().getAnnotation(annotationClass));
		}
		return Optional.absent();
	}

	public static String memberName(AnnotatedMember member) {
		if (member == null || member.getMember() == null) {
			return "";
		}
		return member.getMember().getName();
	}

	@VisibleForTesting
	static ResolvedType getResolvedType(ApiResponse annotation, TypeResolver resolver, ResolvedType defaultType) {

		if (null != annotation) {
			Class<?> response = annotation.response() != Void.class ? annotation.response()
					: EasySwaggerConfiguration.getDefaultModel();
			String responseContainer = annotation.responseContainer();
			if (resolvedType(resolver, response, responseContainer).isPresent()) {
				return resolvedType(resolver, response, responseContainer).get();
			}
		}
		return defaultType;
	}

	private static Optional<ResolvedType> resolvedType(TypeResolver resolver, Class<?> response,
			String responseContainer) {
		if (isNotVoid(response)) {
			if ("List".compareToIgnoreCase(responseContainer) == 0) {
				return Optional.of(resolver.resolve(List.class, response));
			} else if ("Set".compareToIgnoreCase(responseContainer) == 0) {
				return Optional.of(resolver.resolve(Set.class, response));
			} else {
				return Optional.of(resolver.resolve(response));
			}
		}
		return Optional.absent();
	}

	private static boolean isNotVoid(Class<?> response) {
		return Void.class != response && void.class != response;
	}
}
